#include <string.h>
#include "rphoton.hpp"

using namespace TDP::Math;

class rpPhotonWorkSerializer
{
	terSafePointer<terMutex> _mtx;
	terFileStream *_stream;

public:
	rpPhotonWorkSerializer()
	{
		_mtx = ThreadManager->CreateMutex();
	}

	void SetStream(terFileStream *fs)
	{
		_stream = fs;
	}

	void WritePhotonImpact(const terVec3 &impactPoint, const terVec3 &direction, const terVec3 &color, const terVec3 &normal)
	{
		rpPhotonImpactData upid;
		if(color[0] > color[1])
		{
			if(color[2] > color[0])
				upid.intensity = color[2];
			else
				upid.intensity = color[0];
		}
		else
		{
			if(color[2] > color[1])
				upid.intensity = color[2];
			else
				upid.intensity = color[1];
		}

		if(upid.intensity == 0)
			upid.intensity = 1.0f;

		terVec3 normalizedDir = direction.Normalize2();
		for(int i=0;i<3;i++)
		{
			upid.color[i] = (unsigned char)(color[i] / upid.intensity * 255.0f);
			upid.direction[i] = (char)(normalizedDir[i] * 127.0f);
		}

		upid.location = impactPoint;

		float lambertBias = -normalizedDir.DotProduct(normal);

		if(1.0f / lambertBias > config.maxUnbiasAmplification)
			upid.biasedIntensity = upid.intensity / config.maxUnbiasAmplification;
		else
			upid.biasedIntensity = upid.intensity / lambertBias;

		_mtx->Acquire();
		_stream->Write(&upid, sizeof(rpPhotonImpactData));
		_mtx->Release();
	}
};

static terVector<rpPhotonWorkSerializer> workSerializers;

// Each cast produces a single photon
// If the photon impacts a surface, up to two additional traces are produced:
// - One is deflected with half energy, colored by the impact point, multiplied by alpha
// - One is fired through the face, multiplied by one minus alpha
struct rpPhotonShot
{
	Vec<3> color;
	Line<3> path;
	unsigned char *facesTested;

	// Calculated fields
	bool impacted;
	float impactDistanceFromOrigin;	// Used for sorting
	Vec<3> impactPoint;
	int impactChart;				// Index of the chart that was hit by the photon
	Vec<3> impactPassThroughColor;	// Color of light passing through the impact point
	Vec<3> impactDiffuseColor;		// Color of light diffused off of the impact point
	Vec<3> impactDeflectionNormal;
	unsigned int impactModelIndex;
	Vec<3> trajectory;

	// If a photon hits a non-world model:
	// - Pass-through cast is spawned, which (along with its derivatives) ignore that model
	// - Impact is recorded on the model
	// - Normal deflection/pass-through casts are spawned which ignore all other models and
	//   bounce off of the world, but only record impacts on the target model

	// An impact will be recorded on that model's chart, and additional deflections and passthroughs
	// will occur that ONLY affect that model

	// If onlyAffectModelIndex is non-zero, then only impacts on 
	unsigned int onlyAffectModelIndex;
	terVector<unsigned int> ignoreModels;

	bool marking;	// True if this lights (false if resolvable from direct cast)
};

void CastPhotonThroughManifold(rpPhotonShot &shot, const rpManifold *manifold, unsigned int faceNum)
{
	{
		unsigned int modelIndex = manifold->modelIndex;
		if(modelIndex != 0)
		{
			// See if this cast should ignore this model
			if(shot.onlyAffectModelIndex)
			{
				if(shot.onlyAffectModelIndex != modelIndex)
					return;		// Collection cast for that model only
			}
			else
			{
				// World-affecting cast
				for(unsigned int i=0;i<shot.ignoreModels.Count();i++)
					if(shot.ignoreModels[i] == modelIndex)
						return;		// Already passed through that model
			}
		}
	}

	Vec<3> center = (manifold->seg->bounds.mins + manifold->seg->bounds.maxs) * 0.5f;
	Sphere<3> cullSphere(center, (center - manifold->seg->bounds.mins).Length());

	terVec3 p1, p2;
	if(cullSphere.IntersectWithLine(shot.path) == 0)
		return;		// Misses this face completely

	terVec3 traj = shot.trajectory.Normalize2();

	unsigned int n = manifold->seg->numTris;
	for(unsigned int i=0;i<n;i++)
	{
		const terBRBSPCommonVert *verts[3];
		int indexes[3];
		for(int j=0;j<3;j++)
		{
			indexes[j] = manifold->triData[i*3+j];
			verts[j] = manifold->vertData + indexes[j];
		}

		Triangle tri(verts[0]->position, verts[1]->position, verts[2]->position);
		Plane triPlane = tri.Plane();

		if(triPlane.Normal().DotProduct(traj) > Epsilon_k) continue;					// Photon shoots against the tri
		if(triPlane.PointSide(shot.path.Point(0)) != Plane::Front_k) continue;	// Photon starts on or behind the tri

		terVec3 intersectionPoint = triPlane.IntersectWithLine(shot.path);
		float dist = intersectionPoint.DotProduct(traj);

		bool miss = false;
		for(unsigned int j=0;j<3;j++)
		{
			terVec3 edge = tri.Point((j+1) % 3) - tri.Point(j);
			terVec3 planeNormal = edge.Cross(triPlane.Normal()).Normalize2();
			Plane clipPlane(planeNormal.DotProduct(tri.Point(j)), planeNormal);

			if(clipPlane.PointSide(intersectionPoint) == Plane::Back_k)
			{
				miss = true;
				break;
			}
		}
		
		if(miss) continue;		// Missed the tri

		if(shot.impacted && dist > shot.impactDistanceFromOrigin) continue;		// Even if it hits the tri, it won't be closer

		shot.impacted = true;
		shot.impactDistanceFromOrigin = dist;
		shot.impactChart = manifold->seg->originalChartIndex;
		shot.impactPassThroughColor = terVec3(0.0f, 0.0f, 0.0f);
		shot.impactDiffuseColor = terVec3(0.7f, 0.7f, 0.7f);
		shot.impactPoint = intersectionPoint;
		shot.impactDeflectionNormal = triPlane.Normal();
		shot.impactModelIndex = manifold->modelIndex;
	}
}

void CastPhotonThroughLeaf(rpPhotonShot &shot, terBRBSPLeaf *leaf)
{
	const unsigned int *lf = leaf->leafFaces;
	unsigned int n = leaf->numLeafFaces;

	for(unsigned int i=0;i<n;i++)
	{
		unsigned int faceNum = lf[i];
		unsigned char bit = (1 << (faceNum & 7));
		unsigned int bitNum = faceNum/8;

		// Has this face been used yet?
		if(!(shot.facesTested[bitNum] & bit))
		{
			shot.facesTested[bitNum] |= bit;

			terListLink<const rpManifold *> *ll = faceManifolds[faceNum].manifoldList.First();

			while(ll)
			{
				CastPhotonThroughManifold(shot, ll->Data(), faceNum);
				ll = ll->Next();
			}
		}
	}
}

void CastPhotonThroughNode(const Line &line, rpPhotonShot &shot, terBRBSPNode *node);

inline void CastPhotonThroughNodeChild(const Line &line, rpPhotonShot &shot, terBRBSPNode *node, int childIndex)
{
	int child = node->children[childIndex];

	if(child < 0)
		CastPhotonThroughLeaf(shot, geometry._leafs + (-child-1));
	else
		CastPhotonThroughNode(line, shot, geometry._nodes + child);
}

void CastPhotonThroughNode(const Line &line, rpPhotonShot &shot, terBRBSPNode *node)
{
	int castMode = 0;

	Plane splitPlane = Plane(node->planeDist, node->planeNormal);
	terVec3 intersectPoint = splitPlane.IntersectWithLine(line);

	Plane::Side p1side = splitPlane.PointSide(line.Point(0));
	Plane::Side p2side = splitPlane.PointSide(line.Point(1));

	if(p1side == Plane::Front_k)
	{
		if(p2side == Plane::Back_k)
		{
			CastPhotonThroughNodeChild(Line(line.Point(0), intersectPoint), shot, node, 0);
			if(!shot.impacted || splitPlane.PointSide(shot.impactPoint) == Plane::Back_k)
				CastPhotonThroughNodeChild(Line(intersectPoint, line.Point(1)), shot, node, 1);
		}
		else
			CastPhotonThroughNodeChild(Line(line.Point(0), line.Point(1)), shot, node, 0);
	}
	else if(p1side == Plane::On_k)
	{
		if(p2side == Plane::Front_k)
			CastPhotonThroughNodeChild(Line(line.Point(0), line.Point(1)), shot, node, 0);
		else if(p2side == Plane::On_k)
		{
			CastPhotonThroughNodeChild(Line(line.Point(0), line.Point(1)), shot, node, 0);
			CastPhotonThroughNodeChild(Line(line.Point(0), line.Point(1)), shot, node, 1);
		}
		else if(p2side == Plane::Back_k)
			CastPhotonThroughNodeChild(Line(line.Point(0), line.Point(1)), shot, node, 1);
	}
	else if(p1side == Plane::Back_k)
	{
		if(p2side == Plane::Front_k)
		{
			CastPhotonThroughNodeChild(Line(line.Point(0), intersectPoint), shot, node, 1);
			if(!shot.impacted || splitPlane.PointSide(shot.impactPoint) == Plane::Front_k)
				CastPhotonThroughNodeChild(Line(intersectPoint, line.Point(1)), shot, node, 0);
		}
		else
			CastPhotonThroughNodeChild(Line(line.Point(0), line.Point(1)), shot, node, 1);
	}
}


void CastPhoton(terRandomizer &rng, rpPhotonShot &shot, int depth)
{
	if(depth > config.maxDeflections)
		return;

	shot.trajectory = shot.path.Trajectory();
	shot.impacted = false;
	memset(shot.facesTested, 0, (geometry._numFaces + 7) / 8);

	CastPhotonThroughNode(shot.path, shot, geometry._nodes);

	if(shot.impacted)
	{
		if(shot.impactModelIndex != 0)
		{
			// This shot hit a model that wasn't the world
			// Redo the entire trace to prevent impact point phenomena
			// Add this model to the ignore list
			rpPhotonShot pShot;
			pShot.color = shot.color;
			pShot.path = shot.path;
			pShot.facesTested = shot.facesTested;
			pShot.trajectory = shot.trajectory;
			pShot.onlyAffectModelIndex = 0;
			pShot.ignoreModels.AllocFlat(shot.ignoreModels.Count(), shot.ignoreModels);
			pShot.ignoreModels.Resize(pShot.ignoreModels.Count() + 1);
			pShot.ignoreModels[pShot.ignoreModels.Count() - 1] = shot.impactModelIndex;
			pShot.marking = shot.marking;
		}

		// Translucent pass-through cast
		rpPhotonShot pShot;
		pShot.color = shot.color * shot.impactPassThroughColor;
		pShot.path = Line(shot.impactPoint, shot.impactPoint + shot.trajectory);
		pShot.facesTested = shot.facesTested;
		pShot.trajectory = shot.trajectory;
		pShot.marking = shot.marking;

		// If this is based on a shot isolated to a particular model, it can only hit that model or the world
		if(shot.onlyAffectModelIndex)
			pShot.onlyAffectModelIndex = shot.onlyAffectModelIndex;
		else
		{
			// This isn't based on a shot that hit a model
			pShot.onlyAffectModelIndex = shot.impactModelIndex;
			// Keep the ignore list in tact if it's still a world-based cast
			if(!shot.impactModelIndex)
				pShot.ignoreModels.AllocFlat(shot.ignoreModels.Count(), shot.ignoreModels);
		}

		// Deflection cast
		rpPhotonShot dShot;

		dShot.trajectory = rng.RandomDirectionLambertian(shot.impactDeflectionNormal) * config.traceDistance;
		dShot.color = shot.color * shot.impactDiffuseColor;
		dShot.path = Line(shot.impactPoint, shot.impactPoint + dShot.trajectory);
		dShot.facesTested = shot.facesTested;
		dShot.onlyAffectModelIndex = shot.impactModelIndex;
		dShot.marking = true;
		
		if(dShot.onlyAffectModelIndex)
			dShot.onlyAffectModelIndex = shot.onlyAffectModelIndex;
		else
		{
			dShot.onlyAffectModelIndex = shot.impactModelIndex;
			if(!shot.impactModelIndex)
				dShot.ignoreModels.AllocFlat(shot.ignoreModels.Count(), shot.ignoreModels);
		}

		// Normally there is a 50% chance of absorb and 50%
		bool absorb = (rng.RandomUInt32() % 2) != 0;

		// Save the impact, unless this was isolated to a model in which case only save it to the hit model
		if(absorb && shot.marking && (!shot.onlyAffectModelIndex || shot.onlyAffectModelIndex == shot.impactModelIndex))
			workSerializers[shot.impactChart].WritePhotonImpact(shot.impactPoint, shot.trajectory.Normalize2(), shot.color, shot.impactDeflectionNormal);

		if(shot.impactPassThroughColor != terVec3(0.0f, 0.0f, 0.0f))
			CastPhoton(rng, pShot, depth+1);

		if(shot.impactDiffuseColor != terVec3(0.0f, 0.0f, 0.0f) && !absorb)
		{
			// Integral over Lambert distribution = 0.5, so either 50% chance to shoot the bias photon, or 50% intensity
			CastPhoton(rng, dShot, depth+1);
		}
	}
}

// Photon mapping job manager
class rpPhotonWorkUnit : public Threading::WorkUnit
{
	terSafePointer<terMutex> _mtx;
	class Threading::Job *_parent;
	terVec3 _color;
	terVec3 _origin;
	terVec3 _trajectory;
	terVector<unsigned char> facesTested;

	// Photons
	terVec3 _impactLocation;
	Line _originalTrace;

public:
	void SetPhotonProperties(const terVec3 &color, const terVec3 &origin, const terVec3 &trajectory)
	{
		_color = color;
		_origin = origin;
		_trajectory = trajectory;
	}

	rpPhotonWorkUnit()
	{
		_mtx = ThreadManager->CreateMutex();
		facesTested.Alloc((geometry._numFaces + 7) / 8);
	}

	void Work()
	{
		facesTested.Fill(0);


		_originalTrace = Line(_origin, _origin + _trajectory);

		// Set up a shot
		rpPhotonShot shot;
		terRandomizer rng(terHash32(0, &_trajectory, sizeof(terVec3)));

		shot.path = _originalTrace;
		shot.trajectory = _trajectory;
		shot.color = _color;
		shot.facesTested = facesTested;
		shot.onlyAffectModelIndex = 0;
		shot.marking = false;

		// Fire
		CastPhoton(rng, shot, 0);

		_mtx->Release();
	}

	bool TryAcquire()
	{
		return _mtx->TryAcquire();
	}
};


class rpPointLightPhotonEmitterJob : public Threading::DecomposableJob
{
	terVec3 _color;
	terVec3 _origin;
	terRandomizer _rng;
	float _scatter;

public:
	rpPointLightPhotonEmitterJob(Threading::JobManager *parent, const terVec3 &origin, const terVec3 &color, float scatter, unsigned int seed)
		: DecomposableJob(parent, config.photonsPoint)
	{
		_color = color;
		_origin = origin;
		_scatter = scatter;
		_rng.Seed(seed);
	}

	~rpPointLightPhotonEmitterJob()
	{
	}

	Threading::WorkUnit *NewWorkUnit()
	{
		return new rpPhotonWorkUnit();
	}

	void PrepareWorkUnit(Threading::WorkUnit *bru)
	{
		rpPhotonWorkUnit *wu = (rpPhotonWorkUnit *)bru;
		terVec3 permuteOffset;
		while(true)
		{
			permuteOffset = terVec3(_rng.RandomFloat(), _rng.RandomFloat(), _rng.RandomFloat()) * 2.0f - 1.0f;
			if(permuteOffset.LengthSquared() <= 1.0f)
				break;
		}
		wu->SetPhotonProperties(_color, _origin + permuteOffset * _scatter, _rng.RandomDirectionHigh(config.traceDistance));
	}
};


void ShootPhotons(const terString &basePath, const LinkedList<rpObject> *objects)
{
	int objID = 0;

	workSerializers.Alloc(geometry._numCharts);
	for(unsigned int i=0;i<geometry._numCharts;i++)
		workSerializers[i].SetStream(fs->OpenUnprotectedWrite(basePath + "_photons/chart" + terString(i) + ".photons"));

	// Create point light shooters
	for(const terListLink<rpObject> *objLL = objects->First();objLL;objLL=objLL->Next())
	{
		objID++;
		const rpObject *obj = objLL->DataPtr();

		const rpObjectProperty *classname = obj->GetProperty("classname");
		const rpObjectProperty *intensity = obj->GetProperty("light");
		if(!intensity)
			intensity = obj->GetProperty("_light");
		const rpObjectProperty *color = obj->GetProperty("_color");
		const rpObjectProperty *origin = obj->GetProperty("origin");

		if(!classname || !origin )
			continue;

		float intensityV = config.defaultPointIntensity;
		if(intensity)
			intensityV = intensity->value.ToFloat();

		terVec3 colorV = terVec3(1.0f, 1.0f, 1.0f);
		if(color)
			colorV = ParseVector(color->value);

		float softV = config.defaultLightEmissionRadius;

		if(classname->value == "light")
		{
			// Add a job for this
			terSafePointer<Threading::Job> emitter =
				new rpPointLightPhotonEmitterJob(jm,
					ParseVector(origin->value),
					colorV * intensityV * config.photonsPointScale / ((float)config.photonsPoint),
					softV,
					terHash32(0, &objID, sizeof(objID))
				);
			jm->AddJob(emitter);
		}
	}

	while(jm->ActiveJobs())
	{
		ThreadManager->SleepThread(1000);
	}

	workSerializers.Free();
}
